查看原文
其他

当心!别再被大小端的问题坑了

菜刀和小麦 小麦大叔 2022-08-22

先说内存

柿子捡软的捏,以前做项目的时候被大小端的问题坑过,那种酸爽就像蓝天白云,晴空万里忽然暴风雨,突如其来的BUG,让原本不充裕的时间更加雪上加霜;虽然很基础,但是能力有限,也难免出现错误和纰漏,请各位大佬们在讨论中无情指正我。


先说内存

程序运行在内存中,计算机中的最小存储单位是Bit,即10的二进制,它可以识别的机器码就是以二进制形式存储的;

内存由多个存储单元组成,每个存储单元都有一个唯一的数字地址字节可寻址内存。每个存储位置可以包含固定数量的二进制数字。

在大多数的现代计算机上,地址的最小数据的长度为8位,称为字节(1 Byte = 8 Bit);

一般计算机中用户程序直接访问的地址是虚拟内存的地址,操作系统内核会根据用户程序访问的虚拟地址,找出页表中对于的物理地址,最终寻址到所需要的数据;

具体如下图所示;

转自wiki

然而,在MCU等裸机开发的环境中,没有MMU,则程序直接访问的是物理内存,所以无论是计算机还是MCU在程序运行中都需要内存作为载体,保存数据和运行程序。

那么,下面再来看是程序以及数据在内存中是以何种形式存储的?

字节

前面提到过,在大多数的现代计算机上,地址的最小数据的长度为8位,称为字节(1 Byte = 8 Bit);至于为什么是8位?看起来似乎有点玄学,并且很吉利的一个数字,但是老外好像没有数字迷信,这里的原因大概是因为这几点;

  • 由于计算机内部最本质需要实现的操作是加法,减法,乘法,除法等运算都能通过加法实现,另外由于最早期设计的加法器是8位;

  • 另外一个原因可以追溯到1956年,IBM公司最早提出字节的概念,随着IBM的壮大,字节便专门用来表示二进制数,其中也包括不少优点;易于以BCD码形式保存;用于保存文本也非常合适,另外世界上大部分语言都可以用小于256个字符(一个字节宽度)来表示,如果一个不够,那就两个,比如中文;

字节和位

字节顺序

在说大小端之前,要先提一下字节顺序(Endianness),它是描述数据以字节为一组在计算机内存中存储顺序的术语。

字节顺序可以是大端顺序(big-endian)或者小端顺序(little-endian);在对多字节数据进行存储时,一般遵循以下规则;

  • 小端:数据的最后一个字节先存储,即LSB
  • 大端:数据的第一个字节先存储,即MSB

数据0x01020304分别在大端机器和小端机器中的存储形式,具体如下图所示;

在大多数情况下,编译器会处理字节顺序,从而避免出现大小端不一致的问题,但是在以下情况下字节顺序就会成为一个问题。

在通讯中,例如网络编程:假设在小端机器上向文件写入整数,然后将此文件传输到大端机器上。如果没有做大小端转换,那么大端机器就会以相反的顺序读取文件

TCP/IP协议中,默认使用的是大端顺序,它与具体的CPU类型、操作系统等无关;

那么如何在程序中快速的区分大小端呢?

大端和小端的区分

下面介绍几种通过C语言实现大小端判断的方法;

第一种通过指针的内存对齐来实现;

函数的形式;

unsigned char check_endian( void )
{
    int test_var = 1;
    unsigned char *test_endian = (unsigned char*)&test_var;

    return (test_endian[0] == 0);
}

宏定义的形式;

static uint32_t endianness = 0xdeadbeef
enum endianness { BIG, LITTLE };

#define ENDIANNESS ( *(const char *)&endianness == 0xef ? LITTLE \
                   : *(const char *)&endianness == 0xde ? BIG \
                   : assert(0))

更加简洁;

#define IS_BIG_ENDIAN (!*(unsigned char *)&(uint16_t){1})

第二种通过结构体和联合体的内存对齐来实现;

#ifndef ORDER32_H
#define ORDER32_H

#include <limits.h>
#include <stdint.h>

#if CHAR_BIT != 8
#error "unsupported char size"
#endif

enum
{
    O32_LITTLE_ENDIAN = 0x03020100ul,
    O32_BIG_ENDIAN = 0x00010203ul,
    O32_PDP_ENDIAN = 0x01000302ul,      /* DEC PDP-11 (aka ENDIAN_LITTLE_WORD) */
    O32_HONEYWELL_ENDIAN = 0x02030001ul /* Honeywell 316 (aka ENDIAN_BIG_WORD) */
};

static const union { unsigned char bytes[4]; uint32_t value; } o32_host_order =
    { { 0123 } };

#define O32_HOST_ORDER (o32_host_order.value)

#endif

当然具体的方法还有很多,本文就先讲到这里。


—— The End —


长按识别二维码关注获取更多内容




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存